"""PROJET DOMINO     TRON Baptiste / BAUDOIN Théo"""

"""Nous avons detaillé avec des commentaires sous forme de #, 
    les éléments que nous avons utilisés mais que nous n'avons
    pas vu en cours"""

from random import randint
from random import shuffle

class Domino:
    
    """
    La class Domino définit les dominos qui seront joués. 
    Les deux chiffres présent sur les dominos sont compris 
    entre 0 et 6. Chaque domino est unique."""
    

    def __init__(self,valeur1,valeur2):
        """ On définit les valeurs des demi-dominos """
        self.chiffre1 = valeur1             
        self.chiffre2 = valeur2

    def __str__(self):
        """ La méthode __str__ va permettre de renvoyer le domino sous la forme d'une chaîne de caractère. """
        return "[{}|{}]".format(self.chiffre1,self.chiffre2)  #format permet d'intégrer les valeurs chiffre1 et chiffre2 dans les deux {}, cela permet de représenter le domino plus facilement. 


    def __repr__(self):
        """ La méthode __repr__ va permettre d'utiliser la méthode str et de renvoyer ainsi à l'utilisateur la
         représentation du domino. """
        return str(self)


    def inverse(self):
        """ La méthode inverse va renvoyer le domino ayant les valeurs de ses demi-dominos inversés. """
        return Domino(self.chiffre2, self.chiffre1)



    def __eq__(self,domino):
        """ Méthode qui va effectuer un test d'équivalence entre deux dominos."""
        if not isinstance(domino,type(self)):
            return False
        else:
            return sorted((self.chiffre1,self.chiffre2)) == sorted((domino.chiffre1,domino.chiffre2)) #la méthode sorted permet de classer les valeurs des dominos dans l'ordre croissant, afin de faciliter leurs comparaisons..


    def __gt__(self,domino):
        """ Cette méthode permet de réaliser un test de comparaison entre deux dominos. Pour cela on commence par tester
        la différence entre la somme des chiffres du domino puis on s'intéresse au demi-domino. """
        if not isinstance(domino,type(self)):
            return False
        if self.somme_chiffres() == domino.somme_chiffres():
            return self.chiffre1 > domino.chiffre1
        else:
            return self.somme_chiffres() > domino.somme_chiffres()


    def somme_chiffres(self):
        """ La méthode somme_chiffres permet de calculer la somme des demi-dominos d'un domino. Utile pour le début de
        la partie, pour savoir quel joueur commence la partie sachant que celui ayant le domino le plus fort commence. """
        return self.chiffre1 + self.chiffre2


    def liste_valeurs(self):
        """ Cette méthode va permettre de mettre sous forme d'une liste les valeurs des demi-dominos. """
        return [self.chiffre1, self.chiffre2]
    
class Main:
    """La class Main permet de créer les mains des joueurs avec leurs dominos. """

    def __init__(self,les_dominos):
        """La méthode permet de crée un ensemble de dominos sous forme de liste """
        self.dominos = list(les_dominos)            


    def __getitem__(self,i):
        """ Méthode qui permet de récupérer un domino de notre liste de domino créer dans l'initialisation de la
        classe Main. """
        return self.dominos[i]


    def __len__(self):
        """ La méthide __len__ permet de renvoyer la longueur de la liste contenant les dominos. Utile pour savoir le
        nombre de dominos dans les mains et la pioche. """
        return len(self.dominos)

    def __str__(self):
        """ Cette méthode va permettre de renvoyer la liste des domino d'une main ou de la pioche sous la forme d'une
        châine de caractère. """
        ligne_domino = " "                                                  # On crée notre ligne de domino pour l'affichage
        for i in range(len(self.dominos)):                                  # Parcourt de la liste de domino
            ligne_domino += "\n{}. {}".format(i + 1, self.dominos[i])       # On ajoute 1 à 1 les domino à notre liste
        return ligne_domino

    def __repr__(self):
        """ Cette méthode permet de renvoyer la liste des dominos à l'aide de la méthode __str__ qui aura au par avant
        transformé le type de la représentation des dominos. """
        return str(self)


    def jouer(self,domino):
        """ La méthode joueur va permettre de jouer un domino en le retirant de la main du joueur. Pour cela on prend
        en paramètre un domino, trouver son positionnement dans notre liste puis on le retire de la liste de domino. """
        index_domino = self.dominos.index(domino) #index permet d'obtenir l'indice de l'élément dans la liste
        self.dominos.remove(domino) #remove supprime l'élément de la liste
        return index_domino


    def ajout_domino_a_la_main(self,domino,i=None):
        """ Cette méthode va permettre de pouvoir ajouer un domino dans la main du joueur après que celui-ci ait pioché
        par défaut nous ajouterons ce domino à la fin de sa main. """
        if i is None:
            i = len(self.dominos)
        self.dominos.insert(i,domino) #insert permet d'insérer un domino dans la liste avec son indice donné

class Pioche(Main):
	""" La class Pioche hérite de la class Main. Cet a dire 
    que la classe Pioche herite des attributs de la class Main 
    afin d'éviter de créer une autre classe similaire a la class Main.    
    On va définir dans cette classe une seule méthode qui va 
    permettre de piocher un domino dans la pioche lorsque le joueur 
    ne peut pas jouer."""

	def piocher(self):
		""" Cette méthode va permettre dans la partie de domino aux joueurs de piocher un domino dans la pioche
		lorsqu'ils ne peuvent pas jouer. """
		return(self.dominos.pop(randint(0,len(self.dominos)-1)))
        

class Plateau:
    """La class Plateau va etre utilisée pour définir la surface de jeu pour la partie de domino. On va aussi définir
    des méthodes dans cette classe permettant de gérer la posibilité de jouer un domino ou non."""

    def __init__(self):
        """ On definie le plateau"""
        self.plateau = []


    def gauche(self):
        """ Cette méthode renvoie la valeur du demi-domino à l'extrémité gauche du plateau."""
        return(self.plateau[0].chiffre1)


    def droite(self):
        """ Cette méthode renvoie la valeur du demi-domino à l'extrémité droite du plateau."""
        return(self.plateau[-1].chiffre2)
    

    def ajouter_gauche(self,domino):
        """ Cette méthode va ajouter un domino sur la partie gauche du plateau. """
        if self.gauche() == domino.chiffre1:
            self.plateau.insert(0,domino.inverse())
        else:
            self.plateau.insert(0,domino)


    def ajouter_droite(self, domino):
        """ Cette méthode va ajouter un domino sur la partie droite du plateau. """
        if self.droite() == domino.chiffre1:
            self.plateau.append(domino)
        else:
            self.plateau.append(domino.inverse())


    def ajouter(self,domino,position):
        """ Cette méthode va ajouter un domino sur un des côtés du plateau en fonction des bords du plateau et du domino
        à jouer. """
        if not self.plateau:  
            self.plateau.insert(0,domino)
        else:
            if position:
                self.ajouter_gauche(domino)
            else:
                self.ajouter_droite(domino)


    def __len__(self):
        """ La méthode __len__ renvoie la taille du plateau, c'est-à-dire le nombre dominos qui ont été posé sur le
        plateau de jeu. """
        return len(self.plateau)


    def __str__(self):
        """ Cette méthode va renvoyer une représentation du plateau avec les domino."""
        les_dominos_plateau = ' '
        for domino in self.plateau:
            les_dominos_plateau += str(domino)
        return(les_dominos_plateau)


    def __repr__(self):
        """ Cette méthode renvoie la représentation du plateau à l'aide de la méthode __str__."""
        return str(self)

class Partie:
	""" La class Partie permet ici de définir l'ensemble des méthodes qui vont permettre de dérouler l'ensemble d'une
	partie de domino. """


	def __init__(self, plateau,main,pioche):
		self.plateau = plateau
		self.mains = main
		self.tour = None
		self.passe = 0
		self.gagnant = None
		self.pioche = pioche


	@classmethod #classmethod renvoie une méthode de classe pour une fonction donnée
	def nouvelle_partie(cls,nb_joueurs):
		""" Méthode qui va créer une nouvelle partie de jeu. On va alors définir un plateau de jeu, une pioche, la partie,
		et enfin créer les mains des joueurs. """
		plateau = Plateau()
		mains,pioche = distribution_dominos(nb_joueurs)
		partie = cls(plateau,mains,pioche)
		return(partie)


	@staticmethod #renvoie une méthode statique pour une fonction donnée, une méthode statique est  une méthode liée à la classe et non à l’objet de la classe
	def affichage_instructions():
		""" La méthode affichage_instructions va afficher les instructions du jeu, c'est-à-dire les règles du
		jeu pour une partie de domino. """

		print("""\nDébut de la partie de domino:\n\n\
	    Le jeu peut être joué par 2 joueurs. Grâce à un ensemble de 28 dominos, chaque joueur a une main de dominos
	    qui est formée au hasard. Pour une partie à 2 joueurs, chaque joueur a 7 dominos.
	    Le premier à commencer la partie de domino est le joueur qui possède le domino le plus fort. Il doit alors déposer ce même
	    domino sur le plateau au début de la partie. Puis chaque joueur joue à tour de rôle. Si jamais un des joueurs ne peut pas 
	    poser un domino sur le plateau il doit alors tirer un domino dans la pioche jusqu'à ce qu'il puisse joueur ou que la pioche
	    soit vide.Si la pioche est vide, le joueur doit passer son tour, et pourra jouer au tour suivant.\n\n "
		"La partie se termine dans deux conditions :\n\
	    \t1. Si un joueur a déposé tous ses dominos et a vidé sa main. Ce joueur est le gagnant.\n\
	    \t2. Si aucun joueur ne peut jouer un domino, la partie\n\
	    \t   est arrêtée. Le joueur à qui a le moins de dominos dans sa main est le gagant.""")


	def affiche_etat_mains(self):
		""" Cette méthode va afficher les informations sur les mains des joueurs. On va afficher le nombre de dominos et
		les dominos dans les mains de chaque joueurs."""
		for i in range(len(self.mains)):
			print("\nJoueur " + str(i + 1))
			print('Nombre de dominos: ' + str(len(self.mains[i])))
			print("Nombre de dominos dans la pioche: {}".format(len(self.pioche)))


	def affiche_informations_debut_tour(self):
		"""Méthode qui va permettre d'aafficher les informations utiles à chaque début de tour."""
		print("\nTour du joueur {}".format(self.tour+1))    
		self.affiche_etat_mains()                           
		print("\nPlateau: {}".format(self.plateau))         


	@property # @property permet d’accéder à une méthode en tant qu’attribut plutôt qu’en tant que méthode ou fonction
	def premier_joueur(self):
		""" La méthode premier_joueur va déterminer qui est le premier joueur qui commence la partie. Pour cela elle
		détermine qui possède le plus grand domino. """
		numero_joueur = 0
		domino = Domino(0,0)
		for i in range(len(self.mains)):       
			if max(self.mains[i]) > domino:     
				numero_joueur = i
				domino = max(self.mains[i])
		return(numero_joueur,domino)           


	def tour_du_premier_joueur(self):
		""" Méthode qui va réalisé les étapes du tour du premier joueur, en utilisant les méthodes précédentes. Dans cette
		méthode on va jouer le domino du premier joueur sur le plateau. """
		joueur,domino = self.premier_joueur     
		self.tour = joueur
		print("\nLe joueur {} commence avec le domino {}.".format(joueur+1,domino))
		self.plateau.ajouter(domino,False)      
		self.mains[joueur].jouer(domino)        
		self.passer_prochain_joueur()           


	def domino_peut_etre_joue(self,domino):
		""" Cette méthode va vérifier si le domino que le joueur veut jouer peut être poser sur le plateau en analysant
		les demi-dominos qui sont posés sur le plateau. """
		return(self.plateau.droite() in domino.liste_valeurs() or self.plateau.gauche() in domino.liste_valeurs())


	def jouer_gauche_ou_droite(self,domino):
		""" Cette méthode sera utilisé si un domino peut être jouer des deux cotés du plateau, on demande alors à l'utilisateur
		s'il veut jouer le domino à gauche ou à droite du plateau. """
		cote = input("\nDe quel côté voulez-vous le domino?\n1. Gauche\n2. Droite\nRéponse : ")     
		while cote not in ['1', '2']:                                                               
			print("\nChoix non valide. Recommencez.")
			cote = input('1. Gauche\n2. Droite\n Réponse : ')
		if cote == '1':
			self.jouer_gauche(domino)                                                               
		else:
			self.jouer_droite(domino)                                                               


	def jouer_gauche(self, domino):
		""" Méthode qui va permetre au joueur de jouer son domino sur le cote gauche du plateau. """
		print("\nLe joueur {} joue le domino {} à gauche du plateau.".format(self.tour + 1, domino))
		self.plateau.ajouter(domino,True)       
		self.mains[self.tour].jouer(domino)    

	def jouer_droite(self, domino_a_jouer):
		""" Méthode qui va permetre au joueur de jouer son domino sur le cote droit du plateau. """
		print("\nLe joueur {} joue le domino {} à droite du plateau.".format(self.tour + 1, domino_a_jouer))
		self.plateau.ajouter(domino_a_jouer,False)      
		self.mains[self.tour].jouer(domino_a_jouer)     


	def passer_prochain_joueur(self):
		"""Méthode qui va faire passer le jeu au joueur suivant."""
		if self.tour == len(self.mains)-1:
			self.tour = 0
		else:
			self.tour += 1


	def faire_passer_joueur(self):
		"""Méthode qui va faire qu'un joueur peut passer son tour. Pour cela, on va tout d'abord lui faire piocher des
		dominos jusqu'à ce que la pioche soit vide"""
		while not self.joueur_joue_ou_passe():      
			if len(self.pioche):        
				print("\nLe joueur {} ne peut pas jouer. Il doit tirer un domino dans la pioche.".format(self.tour+1))
				domino = self.pioche.piocher()          
				print("\nLe joueur {} prend le domino {} dans la pioche et l'ajoute à sa main.".format(self.tour + 1,domino))
				self.mains[self.tour].ajout_domino_a_la_main(domino)        
			else:
				print("\nLa pioche est vide, et le joueur {} ne peut pas jouer. Il doit passer son tour.".format(self.tour + 1))
				self.passe += 1     
				break               
		if self.joueur_joue_ou_passe():
			print("\nLe joueur {} peut jouer.".format(self.tour+1))
			self.jouer_un_domino()  


	def joueur_joue_ou_passe(self):
		""" Cette méthode va permettre de déterminer si un joueur peut jouer un domino en analysant les dominos de sa main
		et les dominos sur le plateau de jeu. Elle renvoie un booléen en fonction de la possibilité ou non de jouer un
		domino. """
		for domino in self.mains[self.tour]:           
			if self.domino_peut_etre_joue(domino):      
				return(True)                            
		return(False)                                   


	def tour_du_prochain_joueur(self):
		""" Méthode qui va exécuter l'ensemble d'un tour d'un joueur, sauf celui du premier joueur."""
		self.affiche_informations_debut_tour()      
		peut_jouer = False                          
		for domino in self.mains[self.tour]:        
			if self.plateau.gauche() in domino.liste_valeurs() or self.plateau.droite() in domino.liste_valeurs():
				peut_jouer = True
				break
		if peut_jouer:                              
			self.passe = 0
			self.jouer_un_domino()
			self.verifie_gagnant()
		else:                                      
			self.faire_passer_joueur()
		self.passer_prochain_joueur()               


	def demander_numero_domino_a_jouer(self):
		""" Cette méthode va permettre de faire la demande en console au joueur de choisir le domino qu'il veut poser sur
		le plateau de jeu. On vérifie ensuite si le choix est valable ou non. """
		print("\nMain du joueur {}:".format(self.tour+1))              
		print(self.mains[self.tour])
		choix = input("\nQuel domino choisissez vous jouer?")           
		while not (choix.isnumeric() and int(choix) in range(1,len(self.mains[self.tour])+1)): #isnumeric vérifie que un numéro de domino est bien donné
			print("\nLe choix est incorrecte. Veuillez recommencer.")
			choix = input("Quel domino shouaitez-vous jouer?")
		choix = int(choix)-1                                            
		return(self.mains[self.tour][choix])                           

	def jouer_un_domino(self):
		""" Méthode qui va permettre de joueur un domino, pour cela on va tester les dominos sur le plateau et demander
		le choix de domino à l'utilisateur. Puis on va essayer de placer le domino sur le plateau automatiquement. """
		domino_a_jouer = self.demander_numero_domino_a_jouer()
		
		while self.plateau.gauche() not in domino_a_jouer.liste_valeurs() and self.plateau.droite() not in domino_a_jouer.liste_valeurs():
			print("\nCe domino ne peut pas être joué pour l'instant.")
			print("\nPlateau: {}".format(self.plateau))
			domino_a_jouer = self.demander_numero_domino_a_jouer()
		
		if self.plateau.gauche() in domino_a_jouer.liste_valeurs() and self.plateau.droite() in domino_a_jouer.liste_valeurs():
			self.jouer_gauche_ou_droite(domino_a_jouer)        
		
		elif self.plateau.gauche() in domino_a_jouer.liste_valeurs():
			self.jouer_gauche(domino_a_jouer)
		
		else:
			self.jouer_droite(domino_a_jouer)


	def verifie_gagnant(self):
		""" Cette méthode va vérifier si le joueur actuel à gagner la partie, càd on va vérifier si sa main est vide."""
		if not len(self.mains[self.tour]):  
			self.gagnant = self.tour+1


	def joueurs_avec_moins_de_dominos(self):
		""" Cette méthode va parcourir l'ensemble des dominos de chaque joueur et regarder celui qui a le plus petit nombre
		de domino en main. Ce joueur sera déclaré gagnant en cas d'égalité. """ 
		longueur_minimum_main = 15                              
		gagnant = []
		for i in range(len(self.mains)):                        
			if len(self.mains[i]) < longueur_minimum_main:
				longueur_minimum_main = len(self.mains[i])
				gagnant = [i+1]
			elif len(self.mains[i]) == longueur_minimum_main:        
				gagnant.append(i+1)
		return(gagnant)


	def egalite(self,indice):
		""" Cette méthode va simplement renvoyé un message en console si jamais deux joueurs sont à égalité à la fin de
		la partie. """
		print('\nIl y a une égalité entre plusieurs joueurs. Les gagnants sont :', end=' ')
		for i in range(len(indice)):    
			if i == len(indice)-1:
				print('et {}!'.format(indice[i]))
			else:
				print('{},'.format(indice[i]), end=' ')


	def victoire(self):
		""" Cette méthode va afficher un message de victoire, si un joueur gagne la partie de domino."""
		print("\nLe joueur {} n'a plus de domino. Il gagne la partie!".format(self.gagnant))



	def jouer(self):
		""" C'est la méthode principale, elle gère le déroulement complet d'une partie de domino."""
		self.affichage_instructions()                                     # On affiche les règles du jeu
		self.tour_du_premier_joueur()                                   # On fait jouer le premier joueur
		while self.gagnant is None:                                     # On parcours tant qu'il n'y a pas de gagnant
			self.tour_du_prochain_joueur()                              # Joueur suivant joue
			if self.passe == len(self.mains):                           # Lorsque la pioche est vide on cherche un gagnant
				self.gagnant = self.joueurs_avec_moins_de_dominos()
		if isinstance(self.gagnant,int) or len(self.gagnant) == 1:      # On vérifie que l'on a un gagnant
			if not isinstance(self.gagnant,int):
				self.gagnant = self.gagnant[0]
			self.victoire()                                             # Affiche le message de victoire
		else:
			self.egalite(self.gagnant)      # Affiche le message en cas d'égalité
            
 
def distribution_dominos(nb_joueurs):
	""" Cette fonction va créer l'ensemble des joueurs avec le nombre nb_joueurs, puis va ensuite créer
	l'ensemble des mains de chaque joueurs de façon aléatoire en utilisant nb_joueurs. Puis cette fonction va
	aussi créer la pioche en faisant appel à la classe Pioche. """

	ensemble_des_dominos = []
	ensemble_mains = []

	for i in range(6, -1, -1):
		for j in range(i, -1, -1):
			ensemble_des_dominos.append(Domino(i, j))
	shuffle(ensemble_des_dominos) #shuflle mélange l'ensemble de domino
	
	if nb_joueurs == 2:
		nombre_dominos_distribue = 7
	
	for i in range(nb_joueurs):
		ensemble_mains.append(Main(ensemble_des_dominos[i * nombre_dominos_distribue:(i + 1) * nombre_dominos_distribue]))
	pioche = Pioche(ensemble_des_dominos[nombre_dominos_distribue * nb_joueurs:])
	return (ensemble_mains, pioche)

print("Jouons une partie de domino!\n")

nb_joueurs = 2
partie = Partie.nouvelle_partie(nb_joueurs)
partie.jouer()

input('Appuyer sur ENTER pour quitter.')